전역 변수를 사용하지 않고 객체를 하나만 생성하도록 하며, 생성된 객체를 어디에서든 참조할 수 있도록 하는 패턴
위에 대부분 나온 부분이지만 간단히 다시 정리해보겠습니다.
여러 가게에서 한 물류센터에 A 신발을 받아와 판매를 한다고 할 때, 가게 객체에선 A 신발의 객체를 공유하도록 한다고 생각하고 예시를 만들어 보았습니다.
즉 A 신발을 파는 모든 가게에선 물류 센터 내의 신발 개수를 공유합니다.
사실 실제 이런 경우라면 당연히 다른 방법을 쓰겠지만.. 싱글톤 특성인 요소 간 공유 위주로 설명하기 위한 단순한 예를 가져왔습니다.
interface Shoes{
public int getNumberOfShoes();
public String getShoesId();
public void setNumberOfShoes(int numberOfShoes);
public void setShoesId(String shoesId);
}
class ShoesA implements Shoes{
private static ShoesA shoesA = new ShoesA();
private String shoesId;
private int numberOfShoes;
private ShoesA(){}
public static ShoesA getInstance(){
return ShoesA.shoesA;
}
public int getNumberOfShoes(){
return this.numberOfShoes;
}
public String getShoesId(){
return this.shoesId;
}
public void setNumberOfShoes(int numberOfShoes){
this.numberOfShoes = numberOfShoes;
}
public void setShoesId(String shoesId) {
this.shoesId = shoesId;
}
}
class Store{
ArrayList<Shoes> shoeses;
public Store(){
this.shoeses = new ArrayList<Shoes>();
}
public void addShoes(Shoes shoes) {
// 한 신발에 대한 객체는 한 번만 저장
if(!shoeses.contains(shoes))
shoeses.add(shoes);
}
public void sellShoes(Shoes shoes, int number){
shoes.setNumberOfShoes(shoes.getNumberOfShoes()-number);
}
public void printShoeses(){
System.out.println(this);
for(Shoes shoes:shoeses){
System.out.println(shoes.getShoesId()+": "+shoes.getNumberOfShoes());
}
}
}
class Main {
public static void main(String[] args) {
Store ShoesMarket = new Store();
Store ShoesWorld = new Store();
// 물류센터
ShoesA.getInstance().setShoesId("asd1234");
ShoesA.getInstance().setNumberOfShoes(1024);
// 가게에서 신발A 판매 시작
ShoesMarket.addShoes(ShoesA.getInstance());
ShoesWorld.addShoes(ShoesA.getInstance());
// 재고 확인
ShoesMarket.printShoeses();
ShoesWorld.printShoeses();
// 신발 판매
ShoesA shoesA = ShoesA.getInstance();
ShoesMarket.sellShoes(shoesA, 3);
// 재고 확인
System.out.println("----------ShoesMarket에서 신발A 3켤레 판매 후----------");
ShoesMarket.printShoeses();
ShoesWorld.printShoeses();
}
}
Store@27716f4
asd1234: 1024
Store@8efb846
asd1234: 1024
----------ShoesMarket에서 신발A 3켤레 판매 후----------
Store@27716f4
asd1234: 1021
Store@8efb846
asd1234: 1021
한 가게에서 신발A를 팔았을 때 다른 가게에서도 신발A에 대한 수량이 바뀐 것을 확인할 수 있습니다.
위와 다르게 멀티 스레드에서의 싱글톤을 알아보기 위해 더 단순한 형태로 두 클래스를 만들었습니다.
class Person extends Thread {
int id;
public Person(int id) {
this.id = id;
}
public void run() {
Apple.getInstance().print("Person ID: " + this.id + " / Apple: " + Apple.getInstance().toString());
}
}
class Apple{
private static Apple apple = new Apple();
private Apple() {}
public static Apple getInstance() {
return apple;
}
public void print(String str) {
System.out.println(str);
}
}
class Main {
public static void main(String[] args){
int maxStore = 10;
Person[] People = new Person[maxStore];
for (int i = 0; i < maxStore; i++) {
People[i] = new Person(i);
People[i].start();
}
}
}
Person ID: 6 / Apple: Apple@c7b723d
Person ID: 1 / Apple: Apple@c7b723d
Person ID: 9 / Apple: Apple@c7b723d
Person ID: 5 / Apple: Apple@c7b723d
Person ID: 2 / Apple: Apple@c7b723d
Person ID: 4 / Apple: Apple@c7b723d
Person ID: 8 / Apple: Apple@c7b723d
Person ID: 3 / Apple: Apple@c7b723d
Person ID: 7 / Apple: Apple@c7b723d
Person ID: 0 / Apple: Apple@c7b723d
여러 스레드에서 호출했지만 싱글톤 특성에 맞게 같은 객체가 반환됨을 확인할 수 있습니다.
이때 Apple 내의 공통적인 필드를 여러 스레드에서 수정한다면 어떻게 될까요?
class Apple {
private static Apple apple = new Apple();
private int count = 0;
private Apple() {
}
public static Apple getInstance() {
return apple;
}
public void print(String str) {
count++;
System.out.println(str + " / Count: " + this.count);
}
}
count 변수를 추가하고 print 메소드에서 수정 후 함께 출력해줍니다.
위와 동일한 Main을 돌리면 다음처럼 출력됩니다.
Person ID: 0 / Apple: Apple@1db55e5d / Count: 1
Person ID: 1 / Apple: Apple@1db55e5d / Count: 5
Person ID: 3 / Apple: Apple@1db55e5d / Count: 4
Person ID: 4 / Apple: Apple@1db55e5d / Count: 4
Person ID: 5 / Apple: Apple@1db55e5d / Count: 6
Person ID: 2 / Apple: Apple@1db55e5d / Count: 2
Person ID: 6 / Apple: Apple@1db55e5d / Count: 7
Person ID: 9 / Apple: Apple@1db55e5d / Count: 8
Person ID: 8 / Apple: Apple@1db55e5d / Count: 8
Person ID: 7 / Apple: Apple@1db55e5d / Count: 9
Count 부분을 보면 같은 Apple 객체를 사용하지만 전혀 동기화가 되지 않는 모습을 볼 수 있습니다.
synchronized 키워드를 여러 스레드가 동시에 접근하지 못하게 할 곳 (동기화가 필요한 부분)에 추가합니다.
저는 count에 대한 수정이 있는 print 메소드에 추가했습니다.
class Apple {
private static Apple apple = new Apple();
private int count = 0;
private Apple() {
}
public static Apple getInstance() {
return apple;
}
public synchronized void print(String str) {
count++;
System.out.println(str + " / Count: " + this.count);
}
}
다시 동일한 Main을 run하면 다음처럼 출력 됩니다.
Person ID: 2 / Apple: Apple@73c124cd / Count: 1
Person ID: 9 / Apple: Apple@73c124cd / Count: 2
Person ID: 8 / Apple: Apple@73c124cd / Count: 3
Person ID: 1 / Apple: Apple@73c124cd / Count: 4
Person ID: 5 / Apple: Apple@73c124cd / Count: 5
Person ID: 4 / Apple: Apple@73c124cd / Count: 6
Person ID: 6 / Apple: Apple@73c124cd / Count: 7
Person ID: 7 / Apple: Apple@73c124cd / Count: 8
Person ID: 3 / Apple: Apple@73c124cd / Count: 9
Person ID: 0 / Apple: Apple@73c124cd / Count: 10
count가 접근되는 순서대로 수정되었고, 동기화가 잘 되어 중복이 없음을 확인할 수 있습니다.
정적 클래스란 인스턴스를 만들 수 없는 클래스로, 모든 멤버를 static으로 선언해야 합니다.
전역적으로 접근해야 하는 유틸리티 클래스의 경우 정적 클래스로 만들면 유용하게 사용할 수 있습니다.
인스턴스를 가질 수 없고 메소드만 사용하기 때문에 절차지향적인 함수에 더 가깝습니다. 즉, OOP(Object Oriented Programming)의 원칙을 따라가지 않습니다.
싱글톤 | 정적 클래스 | |
---|---|---|
원리 | 하나의 인스턴스를 생성해 재사용 | 인스턴스를 생성하지 않고 정적 멤버에 접근 |
인터페이스 구현 | 가능 | 불가능 |
OOP | O | X |
Override | 가능 | 불가능 |
Load | 필요할 때 Lazy Initialization 가능 | 정적 바인딩되어 컴파일 시간에 Load |
성능 | 상대적으로 느림 | 정적 바인딩을 이용해 빠름 |
class SingltonEarly{
private static SingltonEarly singltonEarly = new SingltonEarly();
...
}
class SingltonLazy{
private static SingltonLazy singltonLazy;
public static SingltonLazy getInstance(){
if(singltonLazy == null){
singltonLazy = new SingltonLazy();
}
return singltonLazy;
}
...
}
싱글톤 패턴은 제약이 확실해야 하는 만큼 구현이 복잡하고 다양한 방법이 있네요.
직렬화/역직렬화 관련 내용은 빼고 정리했는데도 글이 엄청 길어졌습니다.
직렬화/역직렬화 자체에 대한 이해가 부족해 다음에 다시 보기로 하고 넘겼지만 관련 내용은 참고 링크 중 가장 마지막 링크로 가시면 보실 수 있습니다.
참고
- https://jeong-pro.tistory.com/86
- https://webdevtechblog.com/%EC%8B%B1%EA%B8%80%ED%84%B4-%ED%8C%A8%ED%84%B4-singleton-pattern-db75ed29c36
- https://mygumi.tistory.com/265
- https://javaplant.tistory.com/21
- https://twpower.github.io/227-singleton-pattern-concept-and-example
- https://yaboong.github.io/design-pattern/2018/09/28/thread-safe-singleton-patterns/
감사합니다.
Text by Chaelin. Photographs by Chaelin, Unsplash.